iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0

前言

昨天我們為我們的 API 加上文件了,今天讓我們來實作分頁這個功能吧!

先說說什麼是分頁,分頁的意思是當使用者請求列表的 API 時,一次只給他一部份並讓他可以透過指定範圍或是頁碼的方式去選取他要哪個部分的資料。會需要這個功能的原因是因為當資料庫中存在大量的資料時,如果將它全部列出給請求者,會造成資料庫以及網路傳輸很大的負擔,所以我們一次只給使用者一部分,如果他有需要就可以再請求下個部分的資料。

在 ViewSet 中加入分頁

感謝方便的 DRF 他提供了已經實作好的分頁器可以使用,讓我們把它加到我們既有的 ViewSet 中,打開 server/app/todo/views.py 並依照下方內容修改

-from rest_framework import decorators, response, viewsets
+from rest_framework import decorators, pagination, response, viewsets

from server.app.todo import models as todo_models
from server.app.todo import serializers as todo_serializers


class TaskViewSet(viewsets.ModelViewSet):
    queryset = todo_models.Task.objects.all()
    serializer_class = todo_serializers.TaskSerializer
+   pagination_class = pagination.LimitOffsetPagination

# ...... 以下省略 ......

同時打開 server/settings.py 並修改預設分頁大小設定

# ...... 以上省略 ......

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
+   "PAGE_SIZE": 10,
}

# ...... 以下省略 ...... 

這時候大家打開 Postman 並使用 GET 方法請求 http://127.0.0.1:8000/api/todo/tasks(記得啟動虛擬環境 & server 唷)應該會看到類似這樣的請求

{
    "count": 22,
    "next": "http://127.0.0.1:8000/api/todo/tasks?page=2",
    "previous": null,
    "results": [
        {
            "id": 5,
            "tags": [
                {
                    "id": 1,
                    "name": "T01"
                }
            ],
            "title": "測試任務一",
            "description": "這是一個測試任務",
            "is_finish": false
        },
        "....... 這邊會是 Task 資料,以下就省略了 ......."
    ]
}

到這邊我們就可以發現我們這次的請求只拿回了前十筆的資料,如果我們想要第二頁的資料那我們就必須要請求 http://127.0.0.1:8000/api/todo/tasks?page=2 這樣就可以得到再後面十筆的資料了。

加入全域分頁設定

現在我們已經在 ViewSet 中加入分頁器了,但與設定權限時的邏輯相同這邊只針對當前的 ViewSet 套用了設定,我們現在一樣將分頁的設定套用到全部的 ViewSet 中,讓我們打開 server/app/todo/views.py 並依照下方內容修改

-from rest_framework import decorators, pagination, response, viewsets
+from rest_framework import decorators, response, viewsets

from server.app.todo import models as todo_models
from server.app.todo import serializers as todo_serializers


class TaskViewSet(viewsets.ModelViewSet):
    queryset = todo_models.Task.objects.all()
    serializer_class = todo_serializers.TaskSerializer
-   pagination_class = pagination.LimitOffsetPagination

# ...... 以下省略 ......

我們先把針對單一 ViewSet 套用的設定拿掉,接著打開 server/settings.py 並依照下方內容修改

# ...... 以上省略 ......

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
+   "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
}

# ...... 以下省略 ...... 

這樣我們再試著請求看看,應該每個列表 API 都已經套用分頁了。

警告修正

在分頁都加完成後大家可以到啟動 server 的終端機應該會看到類似這樣的警告訊息

UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: <class 'server.app.todo.models.Task'> QuerySet.
  paginator = self.django_paginator_class(queryset, page_size)

會出現這樣的原因是因為我們的 queryset 並沒有進行排序,但是如果要實作分頁排序就是必須的。原因是因為我們請求列表時第一次他會給我資料的地 1 ~ 10 筆,當我請求 page=2 時他會給我 11 ~ 20 筆,那這時如果我們的排序是亂的就有可能造成每次出來的結果不正確,所以在做分頁前需要先排序。

接著讓我們打開 server/app/task/views.py 依照下方內容進行修改

# ...... 以上省略 ......

class TaskViewSet(viewsets.ModelViewSet):
-   queryset = todo_models.Task.objects.all()
+   queryset = todo_models.Task.objects.order_by("id")
    serializer_class = todo_serializers.TaskSerializer

    def get_serializer_class(self):
        if self.action == "create":
            return todo_serializers.TaskCreateSerializer

        return super().get_serializer_class()

    @decorators.action(methods=["patch"], detail=True)
    def status(self, request, pk):
        task = self.get_object()

        serializer = self.get_serializer(
            task,
            data={"is_finish": not task.is_finish},
            partial=True,
        )
        serializer.is_valid(raise_exception=True)
        serializer.save()

        return response.Response(serializer.data)


class TagViewSet(viewsets.ModelViewSet):
-   queryset = todo_models.Tag.objects.all()
+   queryset = todo_models.Tag.objects.order_by("id")
    serializer_class = todo_serializers.TagSerializer

到這邊我們再次請求一次我們的列表 API 應該會看到剛剛的警告不見了。

P.S. 這邊補充說明一下,有些人會習慣在寫完 order_by 後還是會加上 all 雖然不會有錯誤產生,但其實是不必要的所以可以省略,看起來會簡潔些。

客製化分頁

使用到這邊雖然用頁碼的分頁已經可以正常的使用了,但是我們的分頁大小是在設定中寫死的,不能依照前端的需求自行調整,為了滿足這個功能我們需要客製化一下分頁器。

讓我們先建立資料夾 server/utils 並建立檔案 server/utils/__init__.pyserver/utils/pagination.py,如果不想手動建立可以執行下方指令

mkdir server/utils  # 建立資料夾
touch server/utils/__init__.py  # 建立檔案
touch server/utils/pagination.py  # 建立檔案

並在 server/utils/pagination.py 裡面貼入下方內容

from rest_framework import pagination


class PageNumberPagination(pagination.PageNumberPagination):
    page_size_query_param = "page_size"
    max_page_size = 1000

這邊我們做的事是建立一個 PageNumberPagination 他繼承了 DRF 的 PageNumberPagination 並設定我們可以使用 page_size 這個 query params 來指定分頁的大小,並設定最大的大小不能超過 1000。

接著打開 server/settings.py 並修改

# ...... 以上省略 ......

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
-   "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
+   "DEFAULT_PAGINATION_CLASS": "server.utils.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
}

# ...... 以下省略 ......

上方的修改是告訴 DRF 預設的分頁器要使用我們自己定義的那個分頁器。

這時候我們可以打開 Postman 並使用 GET 請求這個網址 http://127.0.0.1:8000/api/todo/tasks?page_size=3 大家會發現我們的 API 只回傳了 3 筆資料,這樣就完成客製化了。

其他分頁器

除了我們今天示範的使用頁碼的分頁器以外 DRF 其實還提供了其他分頁方法,不過設定的方式都相同唯一的差別是回傳給前端的格式,大家可以依照需求使用,如果有需要可以參考官方文件

結語

今天我們為我們的 API 設定分頁了,他可以避免後面資料變多後造成傳輸與資料庫的負擔。

結束前別忘了檢查一下今天的程式碼有沒有問題,並排版好喔。

ruff check --fix .
black .
pyright .

今天的內容就到這邊了,讓我們期待明天的內容吧。

P.S. 今天的檔案更新可以參考我的 Git Commit 大家可以搭配服用


上一篇
Day16 - 為 API 加上文件
下一篇
Day18 - API 搜尋與排序
系列文
Django REST 大冒險:探索精彩紛呈的 API 開發世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言